﻿using DH.SmartConfig.Protocol;

using System.Net;

namespace DH.SmartConfig.Networking;

public class DatagramBroadcaster : IDatagramBroadcaster
{
    private IDatagramClient _broadcastingSocket;
    private bool _isStarted = false;
    private IPEndPoint _broadcastTarget;

    public DatagramBroadcaster()
    {
        _broadcastingSocket = new DefaultDatagramClient();
    }

    public DatagramBroadcaster(IDatagramClient client)
    {
        _broadcastingSocket = client;
    }

    public async Task BroadcastAsync(SmartConfigContext context, SmartConfigArguments args, CancellationToken cancelToken)
    {
        if (_isStarted)
        {
            throw new InvalidOperationException("Already started");
        }

        try
        {
            this._isStarted = true;

            _broadcastingSocket.Bind(new IPEndPoint(args.LocalAddress, 0));
            _broadcastTarget = new IPEndPoint(IPAddress.Broadcast, context.GetOption<int>(StandardOptionNames.BroadcastingTargetPort));
            var encoder = context.Provider.CreateProcedureEncoder();
            var segments = encoder.Encode(context, args);
            var broadcastBuffer = this.CreateBroadcastBuffer(segments.SelectMany(x => x.Frames));

            await this.BroadcastProcedureAsync(context, segments, broadcastBuffer, cancelToken);
        }
        finally
        {
            _isStarted = false;
        }
    }

    private async Task BroadcastProcedureAsync(
        SmartConfigContext context,
        IEnumerable<Segment> segments,
        byte[] broadcastBuffer,
        CancellationToken userCancelToken)
    {
        var segmentInterval = context.GetOption<TimeSpan>(StandardOptionNames.SegmentInterval);
        while (true)
        {
            userCancelToken.ThrowIfCancellationRequested();

            foreach (var segment in segments)
            {
                userCancelToken.ThrowIfCancellationRequested();

                if (segment.BroadcastingMaxTimes > 0)
                {
                    await this.BroadcastSegmentByTimesAsync(context, segment, broadcastBuffer, userCancelToken);
                }
                else
                {
                    await this.BroadcastSegmentUntilAsync(
                        context, segment, broadcastBuffer, userCancelToken);
                }
                if (segmentInterval > TimeSpan.Zero)
                {
                    await Task.Delay(segmentInterval, userCancelToken);
                }
            }

            if (segmentInterval > TimeSpan.Zero)
            {
                await Task.Delay(segmentInterval, userCancelToken);
            }
        }
    }

    private async Task BroadcastSegmentUntilAsync(
        SmartConfigContext context, Segment segment, byte[] broadcastBuffer, CancellationToken token)
    {
        var segmentInterval = context.GetOption<TimeSpan>(StandardOptionNames.SegmentInterval);
        var endTime = TimeSpan.FromMilliseconds(Environment.TickCount) + segment.BroadcastingPeriod;
        while ((TimeSpan.FromMilliseconds(Environment.TickCount) <= endTime) && !token.IsCancellationRequested)
        {
            await this.BroadcastSingleSegmentAsync(segment, broadcastBuffer, segmentInterval, token);
        }
    }

    private async Task BroadcastSegmentByTimesAsync(
        SmartConfigContext context, Segment segment, byte[] broadcastBuffer, CancellationToken token)
    {
        var segmentInterval = context.GetOption<TimeSpan>(StandardOptionNames.FrameInterval);
        for (int i = 0; i < segment.BroadcastingMaxTimes; i++)
        {
            token.ThrowIfCancellationRequested();
            await this.BroadcastSingleSegmentAsync(segment, broadcastBuffer, segmentInterval, token);
        }
    }

    private async Task BroadcastSingleSegmentAsync(
        Segment segment, byte[] broadcastBuffer, TimeSpan segmentInterval, CancellationToken token)
    {
        token.ThrowIfCancellationRequested();
        foreach (var frame in segment.Frames)
        {
            token.ThrowIfCancellationRequested();
            await _broadcastingSocket.SendAsync(broadcastBuffer, frame, this._broadcastTarget);
            if (segment.FrameInterval > TimeSpan.Zero)
            {
                await Task.Delay(segment.FrameInterval, token);
            }
        }
        if (segmentInterval > TimeSpan.Zero)
        {
            await Task.Delay(segmentInterval, token);
        }
    }

    public byte[] CreateBroadcastBuffer(IEnumerable<ushort> frames)
    {
        var maxLength = frames.Max();
        var bytes = new byte[maxLength];
        for (int i = 0; i < bytes.Length; i++)
        {
            bytes[i] = (byte)'1';
        }
        return bytes.ToArray();
    }

    #region IDisposable Support
    private bool _isDisposed = false; // To detect redundant calls

    public void Close()
    {
        this.Dispose();
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_isDisposed)
        {
            if (disposing)
            {
                _broadcastingSocket.Dispose();
            }
            _isDisposed = true;
        }
    }

    ~DatagramBroadcaster()
    {
        this.Dispose(false);
    }

    // This code added to correctly implement the disposable pattern.
    public void Dispose()
    {
        if (_isStarted)
        {
            throw new InvalidOperationException("Already started.");
        }
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }
    #endregion
}